﻿using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using System.Threading;
using System.Runtime.Serialization.Formatters.Binary;
using System.Net;

namespace RemoteObject
{
    [Serializable]
    public class BlockInfoList : List<BlockInfo>
    {
        public FileIntegrity GetFileIntegrity( long fileLength )
        {
            FileIntegrity fInt = new FileIntegrity();
            fInt.totalFileSize = fileLength;
            foreach (BlockInfo bi in this)
            {
                if (bi.remainDataLength > 0)
                {
                    FileBlock fb = new FileBlock();
                    fb.startPos = bi.currentPos;
                    fb.length = bi.remainDataLength;
                    fInt.remainBlocks.Add(fb);
                }
            }
            return fInt;
        }
        public Boolean Deserialize(String fnNet)
        {
            if (File.Exists(fnNet))
            {
                FileStream fs = null;
                try
                {
                    fs = new FileStream(fnNet, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                    BlockInfoList newBil = (new BinaryFormatter()).Deserialize(fs) as BlockInfoList;
                    this.Clear();
                    this.AddRange(newBil);
                    fs.Close();
                    return true;
                }
                catch
                {
                    this.Clear();
                    if (fs != null)
                        fs.Close();
                }
            }
            return false;
        }

        public Boolean Serialize(String fnNet)
        {
            FileStream fs = null;
            try
            {
                fs = new FileStream(fnNet, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
                (new BinaryFormatter()).Serialize(fs, this);
                fs.Close();
                return true;
            }
            catch
            {
                if (fs != null)
                    fs.Close();
            }
            return false;
        }

        public int GetRemainLength()
        {
            int length = 0;
            foreach (BlockInfo bi in this )
            {
                length += bi.remainDataLength;
            }
            return length;
        }
    }

    public enum FileTransMode
    {
        Download = 0,
        Upload = 1,
    }

    public abstract class FileTransWorker
    {
        public String fnRemote;
        public String fnLocal;

        public Boolean stopFlag = false;
        public Boolean threadFinished = false;
        public String  serverUrl;

        public FileTransJob ownerJob;

        protected BlockInfo blockInfo;  //one of ownerjob.s blockinfolist.
        protected TransferParam param = new TransferParam();

        //for file access proc
        protected FileStream localFileStream = null;
        protected Boolean accessFileError = false;
        protected Boolean fileAccessProcStopped = true;
        protected AutoResetEvent nextfileAccessEvent = new AutoResetEvent(true);

        //for transfer speed
        protected DateTime startTime;
        protected DateTime endTime = Common.UnkownDateTime;
        protected int transferredDataLength = 0;  //this is for one worker

        //virtual method
        public virtual void Run()
        {
            startTime = DateTime.Now;
            stopFlag = false;
            transferredDataLength = 0;
        }

        public void Finish()
        {
            endTime = DateTime.Now;
            threadFinished = true;
        }

        //method
        protected abstract void _FileAccessProc();
        protected void WaitFileAccessProcToStop()
        {
            while (!fileAccessProcStopped)
            {
                stopFlag = true;
                nextfileAccessEvent.Set();
                Thread.Sleep(100);
            }
            ownerJob.UpdateProgressFile( false );
            ownerJob.FinishOneWorker(this);
        }

        protected void FileAccessProc()
        {
            fileAccessProcStopped = false;
            try { _FileAccessProc(); }
            catch { }
            fileAccessProcStopped = true;

            if (localFileStream != null)
            {
                localFileStream.Close();
                localFileStream = null;
            }
        }

        public double GetTransferSpeed()
        {
            DateTime endAt = (endTime != Common.UnkownDateTime ? endTime : DateTime.Now);
            TimeSpan ts =  endAt - startTime;
            return 1.0 * transferredDataLength / 1024 / ts.TotalSeconds;
        }

        public int GetStartPos()
        {
            return blockInfo.originPos;
        }

        public int GetEndPos()
        {
            return blockInfo.currentPos + blockInfo.remainDataLength;
        }

        public void Stop()
        {
            stopFlag = true;
        }

        public double GetProgress()
        {
            float completedLength = blockInfo.currentPos - blockInfo.originPos;
            float totalLength = blockInfo.remainDataLength + completedLength;
            return completedLength / totalLength * 100;
        }
    }

    public abstract class FileTransJob
    {
        public List<FileTransWorker> fileTransWorkers = new List<FileTransWorker>();

        public String fnLocal;
        public String fnRemote;
        public String fnNet;

        public Common.Status jobStatus = Common.Status.Pause;

        public int fileLength = 0;

        protected DateTime lastUpdateProgressTime = DateTime.Now;
        public BlockInfoList remainBlocks = new BlockInfoList(); // track the transfer progress.
        public Boolean useCompressionMode = false;

        public String serverUrl;
        public Site remoteSite;

        public FileTransMode transMode;

        //for transfer speed
        public DateTime startTime;
        public DateTime endTime = Common.UnkownDateTime;
        public int transferredDataLength = 0;  //this may be different from the remainBlocks.

        //virtual method
        public virtual void Start()
        {
            startTime = DateTime.Now;
        }
        
        //method
        public double GetTransferSpeed()
        {
            DateTime endAt = (endTime != Common.UnkownDateTime ? endTime : DateTime.Now);
            TimeSpan ts = endAt - startTime;
            return 1.0 * transferredDataLength / 1024 / ts.TotalSeconds;
        }

        public void Cancel()
        {
            foreach (FileTransWorker worker in fileTransWorkers)
            {
                worker.Stop();
            }
            jobStatus = Common.Status.Pause;
        }

        public String GetFileName()
        {
            return fnLocal.Substring(fnLocal.LastIndexOf('\\') + 1);
        }

        public String GetJobName()
        {
            return GetFileName();
        }

        public void FinishOneWorker(FileTransWorker _worker )
        {
            lock (this)
            {
                _worker.Finish();
                int activeWorkers = 0;
                foreach (FileTransWorker worker in fileTransWorkers)
                {
                    activeWorkers += worker.threadFinished ? 0 : 1;
                }
                if (activeWorkers == 0)
                {
                    endTime = DateTime.Now;
                    Console.WriteLine("Job done with FreeBlocks " + Common.getFreeBlockNum() + " rate: " + (100.0 * transferredDataLength / fileLength ).ToString("f1") + "% " + "Speed: " + this.GetTransferSpeed().ToString("f1") + "KB/s" );
                    if (remainBlocks.GetRemainLength() == 0)
                    {
                        jobStatus = Common.Status.Completed;
                        File.Delete(fnNet); //delete progress file if all finished.
                    }
                }
            }
        }

        public void UpdateProgressFile( Boolean delayUpdate = true )
        {
            if (!delayUpdate)
            {
                _UpdateProgressFile();
            }
            else
            {
                TimeSpan ts = DateTime.Now - lastUpdateProgressTime;
                if (ts.TotalSeconds > 20)
                {
                    _UpdateProgressFile();
                    lastUpdateProgressTime = DateTime.Now;
                }
            }
        }

        private void _UpdateProgressFile()
        {
            lock (this)
            {
                remainBlocks.Serialize(fnNet);
                UpdateInfoToIndexServer();
            }
        }

        private void UpdateInfoToIndexServer()
        {
            FileIntegrity fInt = remainBlocks.GetFileIntegrity(fileLength);
            Common.IndexServerPartner.AddPendingJobForUpdateFileDisInfo(GetFileName(), fInt, remoteSite);
        }

        public int getLocalFileLength()
        {
            if (!File.Exists(fnLocal))
                return -1;

            FileInfo fileInfo = new FileInfo(fnLocal);
            return (int)fileInfo.Length;
        }

        public double GetProgress()
        {
            return 100.0 * ( 1.0 - remainBlocks.GetRemainLength() / fileLength );
        }

        public float GetFileSizeInMB()
        {
            return fileLength / 1024.0f / 1024.0f;
        }

        public Common.Status GetStatus()
        {
            return jobStatus;
        }

        public virtual String GetServerURL()
        {
            return serverUrl;
        }

        public List<FileTransWorker> GetWorkerList()
        {
            return fileTransWorkers;
        }

        public void StartAllWorkers(List<Thread> threadList)
        {
            lock (this)
            {
                if (fileTransWorkers.Count != 0 && threadList.Count != 0)
                {
                    foreach (Thread thread in threadList)
                    {
                        thread.Start();
                    }
                    threadList.Clear();
                }
                else
                {
                    // Probally that, Job begins with Breakpoint resuming. So No worker Thread at all, mark as completed.
                    jobStatus = Common.Status.Completed;
                }
            }
        }
    }

    public class UploadJob : FileTransJob
    {
        public UploadJob(String _fnLocal, String _fnRemote, Site _siteUploadTo)
        {
            transMode = FileTransMode.Upload;

            fnLocal = _fnLocal;
            fnRemote = _fnRemote;

            remoteSite = _siteUploadTo;
            serverUrl = String.Format("tcp://{0}/RemoteObject.RemoteObjectUploader", _siteUploadTo.siteIP);
        }

        public override void Start()
        {
            base.Start();

            if (jobStatus == Common.Status.Working || jobStatus == Common.Status.Completed) return;

            fnNet = fnLocal + Common.UploadNetFileExtension;

            fileLength = getLocalFileLength();
            if( fileLength <= 0 )
                return;

            jobStatus = Common.Status.Working;

            //Considering Breakpoint Resuming
            bool isBreakpointResuming = false;
            List<Thread> threadList = new List<Thread>();

            if( File.Exists(fnNet) && File.Exists(fnLocal) )
            {
                if( remainBlocks.Deserialize(fnNet) )
                {
                    // breakpoint resume
                    isBreakpointResuming = true;
                    Console.WriteLine("Break Point Resuming");
                    foreach (BlockInfo blockInfo in remainBlocks)
                    {
                        if (blockInfo.remainDataLength > 0)
                        {
                            UploadWorker uploader = new UploadWorker(fnRemote, fnLocal, serverUrl, blockInfo, this);
                            if (threadList.Count == 0)
                            {
                                try
                                {
                                    RemoteObject.RemoteObjectUploader remoteObjectUploader = uploader.GetRemoteObject();
                                    if (remoteObjectUploader.PrepareUploadFile(GetFileName(), fileLength) != 1)
                                        break;
                                    uploader.proxyObjectTcpReuse = remoteObjectUploader;
                                }
                                catch (Exception e)
                                {
                                    Console.WriteLine(e.ToString());
                                    break;
                                }
                            }
                            Thread thread = new Thread(new ThreadStart(uploader.Run));
                            threadList.Add(thread);
                        }
                    }
                    StartAllWorkers(threadList);
                }
            }

            if (!isBreakpointResuming)
            {
                // Net Task Begin
                Console.WriteLine("New Task Begin");

                int maxBlocks = Common.GetMaxBlockNumber(fileLength);
                int blockLength = (int)(fileLength + maxBlocks - 1) / maxBlocks;
                for (int i = 0; i < maxBlocks; i++)
                {
                    int startPos = blockLength * i;
                    int dataLength = Math.Min(blockLength, fileLength - startPos);

                    BlockInfo blockInfo = new BlockInfo(i, startPos, startPos, dataLength);
                    remainBlocks.Add(blockInfo);

                    UploadWorker uploader = new UploadWorker(fnRemote, fnLocal, serverUrl, blockInfo, this);
                    if (threadList.Count == 0)
                    {
                        try
                        {
                            RemoteObject.RemoteObjectUploader remoteObjectUploader = uploader.GetRemoteObject();
                            if (remoteObjectUploader.PrepareUploadFile(GetFileName(), fileLength) != 1)
                                break;
                            uploader.proxyObjectTcpReuse = remoteObjectUploader;
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e.ToString());
                            break;
                        }
                    }
                    Thread thread = new Thread(new ThreadStart(uploader.Run));
                    threadList.Add(thread);

                }
                StartAllWorkers(threadList);
            }
        }
    }
}
